# This file is part of trytond-pcw-production module for Tryton. 
# The COPYRIGHT file at the top level of this repository contains 
# the full copyright notices and license terms.

import math
from trytond.model import ModelSingleton, Workflow, ModelView, ModelSQL, fields
from trytond.wizard import Wizard, StateView, Button, StateTransition
from trytond.pyson import If, Eval, Bool, Or, And, Not, Equal
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
from decimal import Decimal

__all__ = ['ProductionOrder', 
           'ProductionOrderRaw', 
           'ProductionOrderFinish']
__metaclass__ = PoolMeta

class ProductionOrder(Workflow, ModelSQL, ModelView):
    "Pcw Production Order"
    __name__ = 'pcw.production.order'

    name = fields.Char('Order No.', size=None, readonly=True, select=True)
    reference = fields.Char('Referencia', select=True)
    date = fields.Date('Order Date',
        states={
            'readonly': Eval('state').in_(['done', 'cancel']),
            'required': True,
            },select=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('approved', 'Approved'),
        ('done', 'Done'),
        ('cancel', 'Cancelled'),
        ], 'State', readonly=True, required=True)
    order_raw_line = fields.One2Many('pcw.production.order.raw', 'order', 'Raw Order Lines', 
        states={'readonly': Eval('state').in_(['done', 'cancel'])})
    order_finish_line = fields.One2Many('pcw.production.order.finish', 'order', 'Finish Order Lines', 
        states={'readonly': Eval('state').in_(['done', 'cancel'])})
    notes = fields.Text('Notes')
    company = fields.Many2One('company.company', 'Company', required=True,
        domain=[
            ('id', If(Eval('context', {}).contains('company'), '=', '!='),
                Eval('context', {}).get('company', 0)),
            ],
        select=True, readonly=True)

    @classmethod
    def __setup__(cls):
        super(ProductionOrder, cls).__setup__()
        cls._order.insert(0, ('id', 'DESC'))
        cls._order.insert(1, ('name', 'DESC'))
        cls._order.insert(2, ('date', 'DESC'))
        cls._sql_constraints = [
            ('name_uniq', 'unique(name, company)', 'Production Reference must be unique per Company!'),
        ]

        cls._error_messages.update({
                'costs_not_equal': "No coincide el total del costo de materia prima ('%s') " 
                    "con el total del costo de los productos terminados ('%s')",
                'existencia_menor': "No hay suficiente existencia de materia prima. Producto: '%s' \n" 
                    "Existencia actual: '%s' - Solicitado: '%s'",
                'delete_cancel': ('Order "%s" must be cancelled before '
                    'deletion.'),
                })

        cls._transitions |= set((
                ('draft', 'approved'),
                ('cancel', 'draft'),
                ('approved', 'done'),
                ('approved', 'cancel'),
                ('done', 'cancel'),
                ))

        cls._buttons.update({
                'cancel': {
                    'invisible': Eval('state').in_(['cancel', 'draft']),
                    },
                'draft': {
                    'invisible': ~Eval('state').in_(['cancel']),
                    'icon': If(Eval('state') == 'cancel', 'tryton-clear','tryton-go-previous'),
                    },
                'approve': {
                    'invisible': Eval('state') != 'draft',
                    'readonly': If(And( 
                                      If(Eval('order_raw_line', False), True, False), 
                                      If(Eval('order_finish_line', False), True, False)
                                    )
                                ,False, True),
                    },
                'confirm': {
                    'invisible': Eval('state') != 'approved',
                    'readonly': If(And( 
                                      If(Eval('order_raw_line', False), True, False), 
                                      If(Eval('order_finish_line', False), True, False)
                                    )
                                ,False, True),
                    },
                })


    @staticmethod
    def default_state():
        return 'draft'

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @classmethod
    def delete(cls, orders):
        for order in orders:
            if order.state in ['done','approved']:
                cls.raise_user_error('delete_cancel', error_args=(str(order.name),))                
        super(ProductionOrder, cls).delete(orders)

    @classmethod
    def set_reference(cls, orders):
        '''
        Fill the name field with the oris production sequence
        '''
        pool = Pool()
        Sequence = pool.get('ir.sequence')
        Config = pool.get('pcw.production.configuration')

        config = Config(1)
        for order in orders:
            if order.name:
                continue
            name = Sequence.get_id(config.production_sequence.id)
            cls.write([order], {
                    'name': name,
                    })

    def get_line_total_cost(self, lines):
        res = 0
        for line in lines:
            quantity = Decimal('%.4f' % line.quantity)
            res = res + (quantity * line.unit_price)
        return res

    def check_order(self, to_done):
        # Verificamos que el costo de materia prima no
        # sea diferente al costo de productos terminados.
        total_raw = self.get_line_total_cost(self.order_raw_line)
        total_finish = self.get_line_total_cost(self.order_finish_line)
        if math.fabs(total_raw - total_finish) > 0.02:
            self.raise_user_error('costs_not_equal', error_args=(str(total_raw),str(total_finish),))

        if not to_done:
            return

        # Verificamos la existencia de la materia prima 
        obj_uom = Pool().get('product.uom')
        for line in self.order_raw_line:
            real_qty = line.quantity
            existencia = line.product.get_quantity_by_uom(line.uom)
            if (existencia - real_qty) < -0.01:
                self.raise_user_error('existencia_menor', 
                    error_args=(line.product.name + ' (Unidad: ' + line.product.default_uom.name + ')', str(existencia),str(real_qty),))


    def create_move(self, line, in_out):
        if line.quantity == 0:
            return None

        Move = Pool().get('pcw.stock.move')

        mv = Move()
        mv.date = self.date
        mv.type = in_out
        mv.reference = self.name
        mv.product = line.product
        mv.quantity = line.quantity
        mv.uom = line.uom
        mv.unit_price = line.unit_price
        mv.company = self.company
        mv.currency = self.company.currency
        mv.order = self
        mv.save()
        if mv.id:
            line.move = mv.id
            line.save()
            line.product.set_last_cost(line.unit_price, line.uom)

        return mv

    def create_moves(self):
        pool = Pool()
        Move = pool.get('pcw.stock.move')
        moves=[]

        for line in self.order_raw_line:
            mv = self.create_move(line, 'out')
            if mv:
                moves.append(mv)  

        for line in self.order_finish_line:
            mv = self.create_move(line, 'in')
            if mv:
                moves.append(mv)

        if moves:
            Move.assign(moves)
            Move.confirm(moves)        

    @classmethod
    def process(cls, orders):
        done = []
        for order in orders:
            if order.state in ('done', 'cancel'):
                continue
            order.create_moves()
            done.append(order)
        if done:
            cls.write(done, {
                    'state': 'done',
                    })

    @classmethod
    @ModelView.button
    @Workflow.transition('cancel')
    def cancel(cls, orders):
        Move = Pool().get('pcw.stock.move')
        for order in orders:
            for line in order.order_raw_line:
                if line.move:
                    Move.cancel([line.move])
                    Move.delete([line.move])
            for line in order.order_finish_line:
                if line.move:
                    Move.cancel([line.move])
                    Move.delete([line.move])

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, orders):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('approved')
    def approve(cls, orders):
        for order in orders:
            order.check_order(False)
        cls.set_reference(orders)

    @classmethod
    @ModelView.button
    @Workflow.transition('done')
    def confirm(cls, orders):
        for order in orders:
            order.check_order(True)
        cls.process(orders)


class ProductionOrderRaw(ModelSQL, ModelView):
    "Pcw Production Order Raw Line"
    __name__ = 'pcw.production.order.raw'

    order = fields.Many2One('pcw.production.order', 'Production Order', ondelete='CASCADE', select=True, required=True)
    product = fields.Many2One('product.product', 'Product',
        domain=[('type', '!=', 'service')],
        states={
            'readonly': ~Eval('_parent_order'),
            }, 
        on_change=['product'], required=True)
    uom = fields.Many2One('product.uom', 'Unit',
        states={
            'required': Bool(Eval('product')),
            'readonly': ~Eval('_parent_order'),
            },
        domain=[
            If(Bool(Eval('product_uom_category')),
                ('category', '=', Eval('product_uom_category')),
                ('category', '!=', -1)),
            ],
        on_change=['uom','product', 'quantity', 'waste_quantity', 'waste_uom', '_parent_order.company'])
    quantity = fields.Float('Quantity', digits=(16, 4),
        states={
            'required': Bool(Eval('product')),
            'readonly': ~Eval('_parent_order'),
            },
        on_change=['uom','product', 'quantity', 'waste_quantity', 'waste_uom'])
    unit_price = fields.Numeric('Unit Price', digits=(16, 4),
        states={
            'required': Bool(Eval('product')),
            })
    suggested_cost = fields.Numeric('Suggested cost', digits=(16, 4))
    waste_quantity = fields.Float('Waste Quantity', digits=(16, 4),
        states={
            'readonly': ~Eval('_parent_order'),
            },
        on_change=['uom','product', 'quantity', 'waste_quantity', 'waste_uom'])
    waste_uom = fields.Many2One('product.uom', 'Waste Unit',
        states={
            'required': Bool(Eval('waste_quantity')),
            'readonly': ~Eval('_parent_order'),
            },
        domain=[
            If(Bool(Eval('product_uom_category')),
                ('category', '=', Eval('product_uom_category')),
                ('category', '!=', -1)),
            ],
        on_change=['uom','product', 'quantity', 'waste_quantity', 'waste_uom'])
    product_uom_category = fields.Function( fields.Many2One('product.uom.category', 'Product Uom Category',
            on_change_with=['product']), 'on_change_with_product_uom_category')
    move = fields.Many2One('pcw.stock.move', 'Move', readonly=True)
    notes = fields.Text('Notes')
    party = fields.Many2One('party.party', 'Provider')

    @classmethod
    def __setup__(cls):
        super(ProductionOrderRaw, cls).__setup__()
        cls._error_messages.update({
                'waste_qty_bigger_than_qty': 'La cantidad de desperdicio no puede ser mayor a la cantidad de procesamiento',
                })

    @staticmethod
    def default_quantity():
        return 1

    @staticmethod
    def default_waste_quantity():
        return 0

    def on_change_with_product_uom_category(self, name=None):
        if self.product:
            return self.product.default_uom_category.id

    def on_change_quantity(self):
        res = {} 
        if not self.uom or not self.product:
            return res
        waste = self.get_waste (self.quantity, self.uom, self.waste_uom, self.waste_quantity)
        res['waste_quantity'] = waste['waste_qty']
        res['waste_uom'] = waste['waste_uom'].id
        return res

    def on_change_waste_quantity(self):
        res = {} 
        if not self.uom or not self.product:
            return res
        waste = self.get_waste (self.quantity, self.uom, self.waste_uom, self.waste_quantity)
        res['waste_quantity'] = waste['waste_qty']
        res['waste_uom'] = waste['waste_uom'].id
        return res

    def on_change_uom(self):
        res = {} 
        if not self.uom or not self.product:
            res['unit_price'] = Decimal(0.0)
            res['suggested_cost'] = Decimal(0.0)
            return res
        waste = self.get_waste (self.quantity, self.uom, self.waste_uom, self.waste_quantity)
        res['waste_quantity'] = waste['waste_qty']
        res['waste_uom'] = waste['waste_uom'].id
        res['unit_price'] = Decimal('%.4f' % self.product.get_average_cost(self.uom))
        res['suggested_cost'] = res['unit_price']
        return res

    def on_change_waste_uom(self):
        res = {} 
        if not self.waste_uom:
            return res
        waste = self.get_waste (self.quantity, self.uom, self.waste_uom, self.waste_quantity)
        res['waste_quantity'] = waste['waste_qty']
        res['waste_uom'] = waste['waste_uom'].id
        return res

    def on_change_product(self):
        res = {}
        res['quantity'] = 1.0
        res['waste_quantity'] = 0
        res['unit_price'] = Decimal(0.0)
        res['suggested_cost'] = Decimal(0.0)
        if not self.product:
            res['uom'] = None
            res['waste_uom'] = None
            return res
        res['unit_price'] = Decimal('%.4f' % self.product.get_average_cost())
        res['suggested_cost'] = res['unit_price']
        res['uom'] = self.product.default_uom.id
        res['waste_uom'] = self.product.default_uom.id
        return res

    def get_waste (self, qty, uom, waste_uom, waste_qty):
        res = {}
        res['waste_qty'] = waste_qty
        res['waste_uom'] = waste_uom
        if uom.id == waste_uom.id:
            if waste_qty > qty:  
                res['waste_qty'] = 0
        if uom.id != waste_uom.id:
            obj_uom = Pool().get('product.uom')
            ws_qty = obj_uom.compute_qty(waste_uom, waste_qty, uom)
            if ws_qty > qty:
                res['waste_uom'] = uom
                res['waste_qty'] = 0
        return res

class ProductionOrderFinish(ModelSQL, ModelView):
    "Pcw Production Order Finish Line"
    __name__ = 'pcw.production.order.finish'

    order = fields.Many2One('pcw.production.order', 'Production Order', ondelete='CASCADE', select=True, required=True)
    product = fields.Many2One('product.product', 'Product',
        domain=[('type', '!=', 'service')],
        states={
            'readonly': ~Eval('_parent_order'),
            }, 
        on_change=['product', '_parent_order.company'], required=True)
    uom = fields.Many2One('product.uom', 'Unit',
        states={
            'required': Bool(Eval('product')),
            'readonly': ~Eval('_parent_order'),
            },
        domain=[
            If(Bool(Eval('product_uom_category')),
                ('category', '=', Eval('product_uom_category')),
                ('category', '!=', -1)),
            ],
        on_change=['uom','product','_parent_order.company'])
    product_uom_category = fields.Function(
        fields.Many2One('product.uom.category', 'Product Uom Category',
            on_change_with=['product']), 'on_change_with_product_uom_category')
    quantity = fields.Float('Quantity', digits=(16, 4),
        states={
            'required': Bool(Eval('product')),
            'readonly': ~Eval('_parent_order'),
            })
    unit_price = fields.Numeric('Unit Price', digits=(16, 4),
        states={
            'required': Bool(Eval('product')),
            })
    suggested_cost = fields.Numeric('Suggested cost', digits=(16, 4))
    move = fields.Many2One('pcw.stock.move', 'Move', readonly=True)
    notes = fields.Text('Notes')

    @staticmethod
    def default_quantity():
        return 1

    def on_change_with_product_uom_category(self, name=None):
        if self.product:
            return self.product.default_uom_category.id

    def on_change_uom(self):
        res = {} 
        if not self.uom or not self.product:
            res['unit_price'] = Decimal(0.0)
            res['suggested_cost'] = Decimal(0.0)
            return res
        res['unit_price'] = Decimal('%.4f' % self.product.get_last_cost(self.uom))
        res['suggested_cost'] = res['unit_price']
        return res

    def on_change_product(self):
        res = {}
        res['quantity'] = 1.0
        res['unit_price'] = 0
        res['suggested_cost'] = 0
        if not self.product:
            res['uom'] = None
            return res
        res['unit_price'] = Decimal('%.4f' % self.product.get_last_cost())
        res['suggested_cost'] = res['unit_price']
        res['uom'] = self.product.default_uom.id
        return res

